04. 불변 활용하기: 안정적으로 동작하게 만들기

📌 Contents

📌 재할당

  • 변수에 값을 다시 할당하는 것을 재할당 또는 파괴적 할당이라고 함
  • 재할당은 변수의 의미를 바꿔 추측하기 어렵게 만들고, 언제 어떻게 변경되었는지 추적하기 힘들게 함
  • final 수식자를 붙여 불변 변수를 만들어서 재할당을 막을 수 있음 (Java)
    • 매개 변수도 final을 이용하여 불변변수로 만들어야 함

📌 가변으로 인해 발생하는 의도하지 않은 영향

  • 인스턴스가 가변이면 다른 부분에 의도하지 않은 영향을 주기 쉬움
  • 의도하지 않게 영향을 끼치는 경우 두 가지와 문제를 개선해 주는 설계 방법에 대해 설명할 것임

사례 1: 가변 인스턴스 재사용하기

  • 가변 인스턴스를 재사용하면 한쪽의 변경이 다른 한 쪽에 영향을 줄 수 있음
  • AttackPower 클래스

    class AttackPower {
      static final int MIN = 0;
      int value; // final을 붙이지 않았으므로 가변
    
      AttackPower(int value) {
        if (value < MIN) {
          throw new IllegalArgumentException();
        }
    
        this.value = value;
      }
    }
    
  • Weapon 클래스

    class Weapon {
      final AttackPower attackPower;
    
      Weapon(AttackPower attackPower) {
        this.attackPower = attackPower;
      }
    }
    
  • AttackPower 인스턴스를 재사용 하는 경우

    AttackPower attackPower = new AttackPower(20);
    
    Weapon weaponA = new Weapon(attackPower);
    Weapon weaponB = new Weapon(attackPower);
    
    weaponA.attackPower.value = 25;
    
    System.out.println("Weapon A attack power: " + weaponA.attackPower.value);
    // output: Weapon A attack power: 25
    System.out.println("Weapon B attack power: " + weaponA.attackPower.value);
    // output: Weapon A attack power: 25
    
  • AttackPower 인스턴스를 개별적으로 생성 하는 경우

      AttackPower attackPowerA = new AttackPower(20);
      AttackPower attackPowerB = new AttackPower(20);
    
      Weapon weaponA = new Weapon(attackPowerA);
      Weapon weaponB = new Weapon(attackPowerB);
    
      weaponA.attackPower.value = 25;
    
      System.out.println("Weapon A attack power: " + weaponA.attackPower.value);
      // output: Weapon A attack power: 25
      System.out.println("Weapon B attack power: " + weaponA.attackPower.value);
      // output: Weapon A attack power: 20
    

사례 2: 함수로 가변 인스턴스 조작하기

  • 예상치 못한 동작은 함수(메서드) 떄문에 발생하기도 함
  • AttackPower 클래스에 공격력을 변화시키는 reinforce 메서드와 disable 메서드 추가

    class AttackPower {
      static final int MIN = 0;
      int value; // final을 붙이지 않았으므로 가변
    
      AttackPower(int value) {
        if (value < MIN) {
          throw new IllegalArgumentException();
        }
    
        this.value = value;
      }
    
      /**
      * 공격력 강화하기
      * @param increment 공격력 증가
      */
      void reinforce(int increment) {
        value += increment;
      }
      /** 무력화하기 */
      void disable() {
        value = MIN;
      }
    }
    
  • 이런 구조에서 reinforce가 잘 동작할 것임

  • 하지만 갑자기 공격력이 0이 되는 일이 종종 발생할 수 있음
  • AttackPower 인스턴스가 다른 스레드에서 실행하면서, AttackPower.disable 메서드를 호출했기 때문임
  • Attacker의 disable 메서드와 reinforce 메서드는 부수 효과 문제를 갖고 있음

부수 효과의 단점

  • 함수의 부수 효과는 '함수가 매개변수를 전달받고, 값을 리턴하는 것' 이외에 외부 상태(인스턴스 변수 등)를 변경하는 것을 가리킴
  • 함수(메서드)에는 주요 작용과 부수 효과가 있음
    • 주요 작용: 함수(메서드)가 매개변수를 전달받고, 값을 리턴하는 것
    • 부수 효과: 주요 작용 이외의 상태 변경을 일으키는 것
      • 인스턴스 변수 변경
      • 전역 변수 변경
      • 매개변수 변경
      • 파일 읽고 쓰기 같은 I/O 조작
  • 부수 효과를 일으키는 코드는 결과를 예측하기 힘들며, 유지 보수하기 힘듬
  • 함수 내부에 선언한 지역 변수의 변경은 함수 외부에 영향을 주지 않기 때문에 부수 효과가 아님

함수의 영향 범위 한정하기

  • 부수 효과가 있는 함수는 영향 범위를 예측하기 어렵기 때문에, 함수가 영향을 주거나 받을 수 있는 범위를 한정하는 것이 좋음
  • 함수는 다음 항목을 만족하도록 설계하는 것이 좋음
    • 데이터(상태)는 매개변수로 받기
    • 상태를 변경하지 않기
    • 값을 함수의 리턴 값으로 돌려주기
  • 즉, 매개변수로 상태를 받고, 상태를 변경하지 않고, 값을 리턴하기만 하는 함수가 이상적임


  • 그러면 메서드에서 인스턴스 변수를 사용하는 것도 안 좋을까?
    • 인스턴스 변수는 불변으로 만들어 영향이 전달되지 않게 할 수 있으므로, 예상치 못한 동작 문제를 회피할 수 있음
    • 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적임

불변으로 만들어서 예기치 못한 동작 막기

  • AttackPower 클래스의 인스턴스 변수 value가 가변적이므로, 부수 효과가 발생할 여지가 있었음
  • '주의해서 코드를 작성할 테니 불변이 아니여도 괜찮을 거야'라는 생각은 스스로를 너무 맹신하는 것
  • 문제를 일으킬 가능성은 항상 존재하고, 코드가 많을수록 가능성은 더욱 커짐
  • 따라서 부수 효과의 여지 자체를 없애기 위해 인스턴스 변수를 불변으로 만들어야 함


  • 개선된 AttackPower 클래스
    : value를 불변 변수로 만들고, reinforce와 disable 메서드에서 AttackPower 인스턴스를 새로 생성해 리턴하기

    class AttackPower {
      static final int MIN = 0;
      int value; // final을 붙이지 않았으므로 가변
    
      AttackPower(int value) {
        if (value < MIN) {
          throw new IllegalArgumentException();
        }
    
        this.value = value;
      }
    
      /**
      * 공격력 강화하기
      * @param increment 공격력 증가
      * @return 증가된 공격력
      */
      AttackPower reinforce(final AttackPower increment) {
        return new AttackPower(this.value + increment.value);
      }
      /**
      * 무력화하기
      * @return 무력화한 공격력
      */
      AttackPower disable() {
        return new AttackPower(MIN);
      }
    }
    
  • 개선된 Weapon 클래스

    class Weapon {
      final AttackPower attackPower;
    
      Weapon(AttackPower attackPower) {
        this.attackPower = attackPower;
      }
    
      /**
      * 무기 강화하기
      * @param increment 공격력 강화
      * @return 강화된 무기
      */
      Weapon reinforce(final AttackPower increment) {
        final AttackPower reinforced = attackPower.reinforce(increment);
        return new Weapon(reinforced);
      }
    }
    
  • 개선된 AttackPower와 Weapon 사용 버전

    AttackPower attackPowerA = new AttackPower(20);
    AttackPower attackPowerB = new AttackPower(20);
    
    Weapon weaponA = new Weapon(attackPowerA);
    Weapon weaponB = new Weapon(attackPowerB);
    
    final AttackPower increment = new AttackPower(5);
    final Weapon reinforcedWeaponA = weaponA.reinforce(increment);
    
    System.out.println("Weapon A attack power: " + weaponA.attackPower.value);
    // output: Weapon A attack power: 20
    System.out.println("Reinforced weapon A attack power: " + reinforcedWeaponA.attackPower.value);
    // output: Reinforced weapon A attack power: 25
    System.out.println("Weapon B attack power: " + weaponA.attackPower.value);
    // output: Weapon A attack power: 20
    

📌 불변과 가변은 어떻게 다루어야 할까

기본적으로 불변으로

  • 변수를 불변으로 만들었을 때의 장점
    • 변수의 의미가 변하지 않으므로, 혼란을 줄일 수 있음
    • 동작이 안정적이게 되므로, 결과를 예측하기 쉬움
    • 코드의 영향 범위가 한정적이므로, 유지 보수가 편리해짐

가변으로 설계해야 하는 경우

  • 성능이 중요한 경우 가변이 필요함
    • 대량의 데이터를 빠르게 처리해야 하는 경우
    • 이미지를 처리하는 경우
    • 리소스에 제약이 큰 임베디드 소프트웨어를 다루는 경우
  • 불변이라면 값을 변경할 때마다 인스턴스를 새로 생성하는데, 크기가 큰 인스턴스를 새로 생성하면서 시간이 오래걸려 성능에 문제가 생긴다면, 불변보다는 가변을 사용하는 것이 좋음
  • 반복문 카운터 등 스코프가 국소적인 경우에 스코프에서만 사용되는 지역 변수는 가변으로 해도 괜찮음

코드 외부와 데이터 교환은 국소화하기

  • 파일을 읽고 쓰는 I/O 조작은 코드 외부의 상태에 의존하고, 웹 애플리케이션도 거의 필수로 데이터베이스를 사용함
  • 코드를 아무리 주의 깊게 작성해도, 파일이나 데이터베이스는 코드 외부에 있는 상태임
  • 파일의 내용은 다른 시스템에 의해서 덮어 쓰일 수 있음
  • 코드 내부에서는 이러한 외부 동작을 제어할 수 없음
  • 최근에는 이러한 영향을 그나마 줄일 수 있게 코드 외부와 데이터 교환을 국소화하는 테크닉을 많이 사용함
  • 예를 들어 데이터베이스의 영속화(데이터베이스에 데이터를 저장하는 것)를 캡슐화 하는 디자인 패턴인 리포지터리 패턴이 있음

❓ Questions

❓ 증가하는 값(increment)도 새로운 객체를 만드는 것은 가변을 불변으로 바꾸는 것과 관련이 있는 것인가?

  • 책의 예제 코드에서 무기에 공격력을 올릴 때 증가하는 값을 int가 아닌 AttackPower로 준다.
  • 그런데 어차피 증가하는 값은 다른 인스턴스에 영향을 안주기 때문에 불변이나 부수효과랑은 관련이 없는 것 같다.
  • 그럼 왜 increment도 기본 int 형이 아니라 AttackPower로 선언해서 넘겨준 것일까?
  • 이 이유는 앞 장에서 설명한 잘못된 값 할당과 관련이 있는 것 같다.
  • int 형이면 음수도 들어갈 수 있는데 증가 값이 음수면 안되기 때문에 int 형이 아닌 AttackPower로 선언 한 것 같다.
  • AttakcPower는 value가 음수가 되지 못하도록 가드가 되어있다.

❓ 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적이다?

  • 메서드에서 인스턴스 변수를 쓰는 것은 인스턴스 변수를 불변으로 만들어 예기치 못한 동작을 막을 수 있어서 괜찮다고 한다.
  • 또한 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적임
  • 부수 효과로 인한 범위를 클래스 내부를 허용한다는 것이 무슨 뜻일까?
  • 클래스 내부의 메서드끼리는 부수효과가 일어나도 괜찮다는 것인가?
  • 예제를 보면 reinforce와 disable 메서드가 같은 인스턴스 변수 value를 변경시키서 서로 부수 효과가 일어나서 문제라고 하고 있는 것 아닌가?


  • 계속 생각을 해보아도 클래스 내부에서 부수효과가 일어나는 것을 막기 위해 인스턴스 변수를 사용할 때 불변으로 만들어 사용하는 것 같은데, 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용한다는 말이 잘 이해가 되지 않는다.
  • 일단은 메서드에서 인스턴스 변수를 사용하는 것은 불변을 이용하여 예기치 못한 동작을 막을 수 있어 괜찮다 정도만 알고 넘어가야 될 것 같다.

❓ mvc 패턴과 리포지토리 패턴

  • mvc 패턴이란 model + view + controller 패턴이다.
  • model은 데이터 쿼리 로직과 비즈니스 로직을 다룬다
  • view는 사용자에게 보여지는 부분을 다룬다
  • controller는 view와 model을 연결해주는 역할을 한다.
  • view와 model은 서로 알지 못하고 controller를 통해서만 통신을 한다.
  • 여기서 model은 데이터 쿼리 로직과 비즈니스 로직을 다룬다고 했는데, 보통 mvc 패턴에서는 service가 있다고 배웠다.
  • model(보통 repository라고 부름)에서는 쿼리를 이용해 데이터베이스에 접근하는 로직만 있다.
  • 불러온 데이터를 받아 가공 등을 하는 비즈니스 로직은 다 service에서 처리를 한다.


  • 리포지토리 패턴은 데이터베이스 관련 로직과 애플리케이션 로직을 분리하는 패턴이라고 책에 나온다.
  • 책을 읽고 mvc 패턴과 리포지토리 패턴이 크게 관련이 되있다고 생각이 들었다.
  • mvc 패턴에서 쿼리를 이용해 데이터를 가져오는 repository와 그 데이터를 처리하는 service로 나눈 것이 리포지토리 패턴을 적용한 것인가?
  • 아니면 데이터를 가져와 처리하는 M과 그 데이터를 보여주는 V를 나눈 것이 리포지토리 패턴을 적용한 것인가?


  • mvc 패턴은 데이터를 다루는 model, 보이는 쪽을 다루는 view 그 사이에 controller가 있는 패턴이다.
  • model 부분에서 반드시 데이터 쿼리 로직(repository)과 비즈니스 로직(service)을 나누는 것은 아닌 것 같다.
  • 하지만 spring 같은 프레임 워크에서는 mvc 패턴에서 repository와 service를 분리한다.
  • 이런 경우 mvc 패턴과 리포지토리 패턴을 모두 적용한 경우인 것 같다.

results matching ""

    No results matching ""